import { isBuffer, isObject, isString } from 'lodash-es'
import { posix as posixPath } from 'path'
import { OutputFile } from '../types'
import { normalizeModuleId, normalizePath, toBuffer } from '../utils'

type RollupOutputBundle = import('rollup').OutputBundle[string]

export const isVirtual = (source: string) => {
  return /[\0\?]/.test(source)
}

const bundleProxyCache = new WeakMap<any, any>()

export const createBundleProxy = (bundle: import('rollup').OutputBundle): Record<string, OutputFile> => {
  let proxy = bundleProxyCache.get(bundle)
  if (!proxy) {
    const cache = new WeakMap<object, OutputFile & { _raw: RollupOutputBundle }>()
    bundleProxyCache.set(
      bundle,
      (proxy = new Proxy(bundle, {
        get: (target, p: string, receiver) => {
          if (typeof p !== 'string') return
          const file: RollupOutputBundle = Reflect.get(target, p, receiver)
          if (file) {
            if (cache.has(file)) {
              return cache.get(file)
            }

            let proxyFile: OutputFile & { _raw: RollupOutputBundle }
            switch (file.type) {
              case 'chunk':
                let fileMeta: any
                cache.set(
                  file,
                  (proxyFile = {
                    get _raw() {
                      return file
                    },
                    get fileName() {
                      return this._raw.fileName
                    },
                    set fileName(value) {
                      if (typeof value !== 'string') return
                      const oldFileName = this._raw.fileName
                      this._raw.fileName = value
                      delete target[oldFileName]
                      target[value] = this._raw
                    },
                    get text() {
                      return (this._raw as any).code
                    },
                    set text(value) {
                      ;(this._raw as any).code = value
                    },
                    get contents() {
                      return Buffer.from((this._raw as any).code)
                    },
                    set contents(value) {
                      ;(this._raw as any).code = value.toString()
                    },
                    get meta() {
                      return (
                        fileMeta ??
                        (fileMeta = {
                          isEntry: file.isEntry,
                          entryPoint: file.isEntry ? file.facadeModuleId!: undefined,
                          imports: [...file.imports, ...file.dynamicImports],
                          exports: [...file.exports]
                        })
                      )
                    }
                  })
                )
                cache.set(proxyFile, proxyFile)
                return proxyFile

              case 'asset':
                cache.set(
                  file,
                  (proxyFile = {
                    get _raw() {
                      return file
                    },
                    get fileName() {
                      return this._raw.fileName
                    },
                    set fileName(value) {
                      if (typeof value !== 'string') return
                      const oldFileName = this._raw.fileName
                      this._raw.fileName = value
                      delete target[oldFileName]
                      target[value] = this._raw
                    },
                    get text() {
                      return ((this._raw as any).source ?? '').toString()
                    },
                    set text(value) {
                      ;(this._raw as any).source = value
                    },
                    get contents() {
                      return toBuffer((this._raw as any).source ?? '')
                    },
                    set contents(value) {
                      ;(this._raw as any).source = value
                    }
                  })
                )
                cache.set(proxyFile, proxyFile)
                return proxyFile
            }
          }
        },
        set: (target, p: string, value) => {
          if (typeof p !== 'string') return false
          if (cache.has(value)) {
            value = cache.get(value)?._raw
            if (value) {
              value.fileName = p
            } else {
              delete target[p]
              return true
            }
          } else if (isString(value) || isBuffer(value)) {
            const asset: import('rollup').OutputAsset = {
              type: 'asset',
              fileName: p,
              source: value,
              isAsset: true
            } as import('rollup').OutputAsset

            value = asset
          } else if (isObject(value)) {
            ;(value as any).fileName = p
            const asset: import('rollup').OutputAsset = {
              type: 'asset',
              fileName: p,
              name: p,
              source: (value as any).contents ?? (value as any).text,
              isAsset: true
            } as import('rollup').OutputAsset

            value = asset
          } else {
            return false
          }

          return Reflect.set(target, p, value)
        }
      }))
    )
  }

  return proxy
}

export const getResolveUrl = (path: string, URL = 'URL') => `(new ${URL}(${path})).href.replace('file:///', '')`

const getRelativeUrlFromDocument = (relativePath: string, umd = false) =>
  getResolveUrl(
    `${JSON.stringify(relativePath)}, ${
      umd ? `typeof document === 'undefined' ? location.href : ` : ''
    }document.currentScript && document.currentScript.src || document.baseURI`
  )

const getResolveCommonPath = (relativePath: string) => {
  const resolvedCommonjsPath = `(__dirname + ${JSON.stringify(normalizePath('/' + relativePath))})`
  const resolvedEsmPath = getResolveUrl(`${JSON.stringify(normalizePath('../' + relativePath))}, import.meta.url`)

  return `(typeof __dirname === 'string' ? ${resolvedCommonjsPath} : ${resolvedEsmPath})`
}

function resolveFileUrl(
  fileName: string,
  chunkFileName: string,
  options: { base?: string; format: import('rollup').InternalModuleFormat; [x: string]: string | undefined }
) {
  let { base } = options
  if (base && typeof base === 'string') {
    let url: string
    if (base.includes(':')) {
      url = new URL(fileName, base).href
    } else if (base.startsWith('//')) {
      url = '/' + normalizePath(posixPath.join(base, fileName))
    } else {
      url = normalizeModuleId(posixPath.join(base, fileName))
    }
    return JSON.stringify(url)
  } else {
    const relativePath = normalizeModuleId(posixPath.relative(posixPath.dirname(chunkFileName), fileName))

    switch (options.format) {
      case 'amd':
        return getResolveUrl(`require.toUrl(${JSON.stringify(relativePath)}), document.baseURI`)
      case 'cjs':
        const resolvedNodePath = getResolveCommonPath(relativePath)
        return `(typeof document === 'undefined' ? ${resolvedNodePath} : ${getRelativeUrlFromDocument(relativePath)})`
      case 'es':
        return getResolveUrl(`${JSON.stringify(relativePath)}, import.meta.url`)
      case 'iife':
        return getRelativeUrlFromDocument(relativePath)
      case 'system':
        return getResolveUrl(`${JSON.stringify(relativePath)}, module.meta.url`)
      case 'umd':
        const amdResolvedUrl = getResolveUrl(`require.toUrl(${JSON.stringify(relativePath)}), document.baseURI`)
        const commonjsResolvedNodePath = getResolveCommonPath(relativePath)
        const iifeResolvedUrl = getRelativeUrlFromDocument(relativePath, true)

        return `(typeof document === 'undefined' && typeof location === 'undefined' ? typeof define === 'function' && define.amd ? ${amdResolvedUrl} : ${commonjsResolvedNodePath} : ${iifeResolvedUrl})`
    }
  }
}
